package com.atguigu.gmall.activity.service.impl;

import com.atguigu.gmall.activity.model.OrderRecode;
import com.atguigu.gmall.activity.model.SeckillGoods;
import com.atguigu.gmall.activity.model.UserRecode;
import com.atguigu.gmall.activity.service.SeckillBizService;
import com.atguigu.gmall.activity.service.SeckillGoodsService;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.DateUtil;
import com.atguigu.gmall.common.util.MD5;
import com.atguigu.gmall.common.util.SnowFlake;
import com.atguigu.gmall.order.client.OrderFeignClient;
import com.atguigu.gmall.order.model.OrderDetail;
import com.atguigu.gmall.order.model.OrderInfo;
import com.atguigu.gmall.product.model.SkuInfo;
import com.atguigu.gmall.rabbit.config.MqConst;
import com.atguigu.gmall.rabbit.service.RabbitService;
import com.atguigu.gmall.user.client.UserFeignClient;
import com.atguigu.gmall.user.model.UserAddress;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.unit.DataUnit;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author: atguigu
 * @create: 2023-09-23 10:25
 */
@Slf4j
@Service
@RefreshScope //配置动态刷新
public class SeckillBizServiceImpl implements SeckillBizService {

    @Autowired
    private Cache<String, String> seckillGoodsCache;

    @Autowired
    private SeckillGoodsService seckillGoodsService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RabbitService rabbitService;

    @Autowired
    private SnowFlake snowFlake;

    @Autowired
    private UserFeignClient userFeignClient;

    @Autowired
    private OrderFeignClient orderFeignClient;


    /**
     * TODO：将盐放入配置文件，动态读取动态刷新
     */
    private static final String salt = "atguigu..";

    /**
     * 为用户秒杀指定商品生成抢购码
     *
     * @param userId
     * @param skuId
     * @return
     */
    @Override
    public String getSeckillSkuIdStr(String userId, Long skuId) {
        //1.验证本地缓存中商品状态位
        String status = seckillGoodsCache.getIfPresent(skuId.toString());
        if (StringUtils.isBlank(status)) {
            throw new RuntimeException("商品不存在");
        }
        if ("0".equals(status)) {
            throw new RuntimeException("商品已售罄");
        }
        if ("1".equals(status)) {
            //2.判断商品是否在销售时间内
            //2.1 查询Redis中秒杀商品信息获取开始、结束时间
            SeckillGoods seckillGoods = seckillGoodsService.getSecGoodsById(skuId);
            if (seckillGoods != null) {
                Date startTime = seckillGoods.getStartTime();
                Date endTime = seckillGoods.getEndTime();

                //2.2 判断时间是否符合要求
                Date now = new Date();
                if (now.getTime() > startTime.getTime() && now.getTime() < endTime.getTime()) {
                    //3.按照指定加密规则生成抢购码 规则：md5(userId+skuId+salt)
                    String buyCode = MD5.encrypt(userId + skuId + salt);
                    return buyCode;
                } else {
                    throw new RuntimeException("秒杀商品不在销售时间内！");
                }
            }
        }
        throw new RuntimeException("生成抢购码有误！");
    }

    /**
     * 秒杀入队，将用户秒杀请求放入MQ队列中
     *
     * @param userId
     * @param skuId
     * @param buyCode
     * @return
     */
    @Override
    public void seckillRequest2Queue(String userId, Long skuId, String buyCode) {
        //1.验证提交抢购码是否正确
        //1.1 按照生成抢购码规则再次生成抢购码
        String serverBuyCode = MD5.encrypt(userId + skuId + salt);
        //1.2 跟用户提交进行比较
        if (!serverBuyCode.equals(buyCode)) {
            throw new RuntimeException(ResultCodeEnum.ILLEGAL_REQUEST.getMessage());
        }
        //2.验证本地缓存中商品状态位是否为1
        String status = seckillGoodsCache.getIfPresent(skuId.toString());
        if (StringUtils.isBlank(status)) {
            throw new RuntimeException("商品不存在");
        }
        if ("0".equals(status)) {
            throw new RuntimeException("商品已售罄");
        }
        if ("1".equals(status)) {
            //3.验证用户是否重复下单（重复购买）
            //3.1 构建用户秒杀订单Key
            String secUserOrderKey = RedisConst.SECKILL_ORDERS_USERS;
            //3.2 创建绑定hash操作对象 key:当日秒杀订单记录  hashKey:用户ID+商品ID  hashVal:订单ID
            BoundHashOperations<String, String, Long> seckUserOrderHashOps = redisTemplate.boundHashOps(secUserOrderKey);
            //3.3 判断用户是否已经提交订单
            String hashKey = userId + ":" + skuId.toString();
            if (seckUserOrderHashOps.hasKey(hashKey)) {
                throw new RuntimeException("请勿重复下单！");
            }

            //4.将用户秒杀意向封装秒杀请求对象(用户ID，商品ID，商品数量)，将秒杀请求放入消息队列
            UserRecode userRecode = new UserRecode();
            userRecode.setUserId(userId);
            userRecode.setSkuId(skuId);
            userRecode.setNum(1);
            rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_SECKILL_USER, MqConst.ROUTING_SECKILL_USER, userRecode);
        }
    }


    /**
     * 监听秒杀队列中请求，完成秒杀核心业务处理
     *
     * @param userRecode 包含：userId,skuId,num
     */
    @Override
    public void processSeckillRequest(UserRecode userRecode) {
        //1.消息幂等性处理（set nx成功正在排队-有机会获取秒杀资格）
        String requeueKey = "seckill:requeue:" + userRecode.getUserId() + ":" + userRecode.getSkuId();
        //TODO 动态计算保存时间，秒杀商品结束时间-当前时间
        Boolean requeue = redisTemplate.opsForValue().setIfAbsent(requeueKey, userRecode.getSkuId().toString(), 10, TimeUnit.HOURS);
        if (!requeue) {
            log.error("【秒杀服务】用户重复进行排队：{}", userRecode);
            return;
        }
        //2.验证本地缓存中商品状态位
        String status = seckillGoodsCache.getIfPresent(userRecode.getSkuId().toString());
        if (null == status || "0".equals(status) || "2".equals(status)) {
            log.error("【秒杀服务】商品已售罄");
            return;
        }

        //3.验证用户是否已经下过单（禁止用户重复下单）
        String secUserOrderKey = RedisConst.SECKILL_ORDERS_USERS;
        //3.2 创建绑定hash操作对象 key:当日秒杀订单记录  hashKey:用户ID+商品ID  hashVal:订单ID
        BoundHashOperations<String, String, Long> seckUserOrderHashOps = redisTemplate.boundHashOps(secUserOrderKey);
        //3.3 判断用户是否已经提交订单
        String hashKey = userRecode.getUserId() + ":" + userRecode.getSkuId().toString();
        if (seckUserOrderHashOps.hasKey(hashKey)) {
            return;
        }

        //4.验证库存（利用Redis命令原子性-List 避免超卖）
        String seckillStockListKey = RedisConst.SECKILL_STOCK_PREFIX + userRecode.getSkuId();
        BoundListOperations<String, String> stockListOps = redisTemplate.boundListOps(seckillStockListKey);
        String stock = stockListOps.rightPop();
        //如果没库存-发送Redis发布订阅消息，通知所有秒杀节点更新本地缓存商品状态位
        if (StringUtils.isBlank(stock)) {
            redisTemplate.convertAndSend("seckillpush", userRecode.getSkuId() + ":0");
            return;
        }
        //5.为获取秒杀资格用户产生临时订单-存入Redis-Hash 获取到秒杀资格
        OrderRecode tempOrder = new OrderRecode();
        tempOrder.setUserId(userRecode.getUserId());
        tempOrder.setNum(userRecode.getNum());
        tempOrder.setSeckillGoods(seckillGoodsService.getSecGoodsById(userRecode.getSkuId()));
        //5.1 生成将来用户保存秒杀订单：订单编号 SEC+年日月+雪花算法
        String today = DateUtil.formatDate(new Date());
        String outTradeNo = "SEC" + today + snowFlake.nextId();
        tempOrder.setOrderStr(outTradeNo);

        String tempOrderKey = RedisConst.SECKILL_ORDERS;
        String tempOrderHashKey = userRecode.getUserId() + ":" + userRecode.getSkuId();
        BoundHashOperations<String, String, OrderRecode> tempOrderHashOps = redisTemplate.boundHashOps(tempOrderKey);
        tempOrderHashOps.put(tempOrderHashKey, tempOrder);

        //5.2 TODO 发送延迟消息通知订单服务延迟关闭秒杀订单-秒杀服务判断订单状态进行恢复秒杀库存


        //6.MQ异步通知秒杀服务-更新秒杀库存（数据最终一致）
        rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_SECKILL_STOCK, MqConst.ROUTING_SECKILL_STOCK, userRecode);
    }

    /**
     * 更新秒杀库存
     * 需要做幂等性处理，需要事务管理
     *
     * @param userRecode
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void processStockDeduct(UserRecode userRecode) {
        //1.幂等性处理
        String skuId = userRecode.getSkuId().toString();
        String requeueKey = "seckill:stock:" + userRecode.getUserId() + ":" + skuId;
        Boolean requeue = redisTemplate.opsForValue().setIfAbsent(requeueKey, skuId.toString(), 10, TimeUnit.HOURS);
        if (!requeue) {
            return;
        }
        //2.更新Redis中Hash秒杀商品库存
        //2.1 查询指定商品库存List长度-商品剩余库存
        String stockKey = RedisConst.SECKILL_STOCK_PREFIX + skuId;
        BoundListOperations<String, String> stockListOps = redisTemplate.boundListOps(stockKey);
        Long stockCount = stockListOps.size();

        //2.2 更新Redis中商品库存
        String seckillHashKey = RedisConst.SECKILL_GOODS;
        BoundHashOperations<String, String, SeckillGoods> seckillHashOps = redisTemplate.boundHashOps(seckillHashKey);
        SeckillGoods seckillGoods = seckillHashOps.get(skuId);
        seckillGoods.setStockCount(stockCount.intValue());
        seckillHashOps.put(skuId.toString(), seckillGoods);

        //3.更新数据库
        seckillGoodsService.updateById(seckillGoods);
    }

    /**
     * 处理前端定时查询秒杀结果
     *
     * @param skuId
     * @return
     */
    @Override
    public Result checkSeckillStatus(String userId, Long skuId) {
        //1.先判断当前用户秒杀指定商品是否正在排队（秒杀请求是否被程序处理中）
        String requeueKey = "seckill:requeue:" + userId + ":" + skuId;

        //1.1 如果在排队中，优先查询用户是否产生秒杀订单-从秒杀订单hash中判断 响应218
        Boolean requeue = redisTemplate.hasKey(requeueKey);
        if (requeue) {
            String orderHashKey = RedisConst.SECKILL_ORDERS_USERS;
            BoundHashOperations<String, String, Long> orderHashOps = redisTemplate.boundHashOps(orderHashKey);
            String hashKey = userId + ":" + skuId;
            Boolean hasOrder = orderHashOps.hasKey(hashKey);
            if (hasOrder) {
                Long seckillOrderId = orderHashOps.get(hashKey);
                return Result.build(seckillOrderId, ResultCodeEnum.SECKILL_ORDER_SUCCESS);
            }
            //1.2 如果在排队中，没有产生秒杀订单，再判断用户是否产生临时订单-从临时订单hash中判断 响应215
            String orderTempHashKey = RedisConst.SECKILL_ORDERS;
            BoundHashOperations<String, String, OrderRecode> orderTempHashOps = redisTemplate.boundHashOps(orderTempHashKey);
            Boolean hasTempOrder = orderTempHashOps.hasKey(hashKey);
            if (hasTempOrder) {
                OrderRecode orderRecode = orderTempHashOps.get(hashKey);
                return Result.build(orderRecode, ResultCodeEnum.SECKILL_SUCCESS);
            }
            //1.3 没有以上两个hash都没有用户记录，返回排队中 响应211
            return Result.build(null, ResultCodeEnum.SECKILL_RUN);
        }
        //2.如果用户未排队且商品状态位已售罄 响应213
        String status = seckillGoodsCache.getIfPresent(skuId.toString());
        if (!requeue && "0".equals(status)) {
            return Result.build(null, ResultCodeEnum.SECKILL_FINISH);
        }
        return Result.build(null, ResultCodeEnum.SECKILL_RUN);
    }


    /**
     * 汇总秒杀确认页面相关数据
     *
     * @param userId
     * @param skuId
     * @return ${userAddressList}:地址列表
     * ${detailArrayList}:订单明细列表
     * ${totalNum}:总数
     * ${totalAmount}:总金额
     */
    @Override
    public Map<String, Object> seckillTradeData(String userId, Long skuId) {
        Map<String, Object> mapResult = new HashMap<>();
        //1.远程调用用户微服务获取收件地址列表
        List<UserAddress> userAddressList = userFeignClient.findUserAddressListByUserId(Long.valueOf(userId));
        if (!CollectionUtils.isEmpty(userAddressList)) {
            mapResult.put("userAddressList", userAddressList);
        }

        //2.查询Redis中当前用户产生临时订单数据-得到秒杀商品信息
        String orderTempHashKey = RedisConst.SECKILL_ORDERS;
        BoundHashOperations<String, String, OrderRecode> orderTempHashOps = redisTemplate.boundHashOps(orderTempHashKey);
        String hashKey = userId + ":" + skuId;
        Boolean hasTempOrder = orderTempHashOps.hasKey(hashKey);
        if (hasTempOrder) {
            //2.1 查询秒杀商品信息
            OrderRecode orderRecode = orderTempHashOps.get(hashKey);
            //2.2 将秒杀商品信息转为订单明细
            OrderDetail orderDetail = new OrderDetail();
            SeckillGoods seckillGoods = orderRecode.getSeckillGoods();
            orderDetail.setSkuId(seckillGoods.getSkuId());
            orderDetail.setSkuName(seckillGoods.getSkuName());
            orderDetail.setImgUrl(seckillGoods.getSkuDefaultImg());
            orderDetail.setSkuNum(orderRecode.getNum());
            orderDetail.setOrderPrice(seckillGoods.getCostPrice());
            List<OrderDetail> orderDetailList = Arrays.asList(orderDetail);
            mapResult.put("detailArrayList", orderDetailList);

            mapResult.put("totalAmount", seckillGoods.getCostPrice());
            mapResult.put("totalNum", orderRecode.getNum());
        }
        return mapResult;
    }

    /**
     * 提交秒杀订单
     *
     * @param orderInfo
     * @return
     */
    @Override
    public Long submitSeckillOrder(OrderInfo orderInfo) {
        //1.远程调用订单服务保存秒杀订单 得到订单ID
        //1.1 查询用户临时下单记录 获取秒杀订单订单编号
        List<OrderDetail> orderDetailList = orderInfo.getOrderDetailList();
        if (!CollectionUtils.isEmpty(orderDetailList)) {
            Long skuId = orderDetailList.get(0).getSkuId();
            String orderTempHashKey = RedisConst.SECKILL_ORDERS;
            String hashKey = orderInfo.getUserId() + ":" + skuId.toString();
            BoundHashOperations<String, String, OrderRecode> orderTempHashOps = redisTemplate.boundHashOps(orderTempHashKey);
            Boolean hasTempOrder = orderTempHashOps.hasKey(hashKey);
            if (hasTempOrder) {
                OrderRecode orderRecode = orderTempHashOps.get(hashKey);
                //设置秒杀订单编号
                orderInfo.setOutTradeNo(orderRecode.getOrderStr());
                Long orderId = orderFeignClient.submitSeckillOrder(orderInfo);

                //2.在Redis中新增用户下单记录

                String orderHashKey = RedisConst.SECKILL_ORDERS_USERS;
                BoundHashOperations<String, String, Long> orderHashOps = redisTemplate.boundHashOps(orderHashKey);
                Boolean hasOrder = orderHashOps.hasKey(hashKey);
                if (!hasOrder) {
                    orderHashOps.put(hashKey, orderId);
                }

                //3.删除Redis中用户临时订单
                orderTempHashOps.delete(hashKey);
                return orderId;
            }
        }
        throw new RuntimeException("订单提交失败！");
    }

    /**
     * 清理当日秒杀缓存数据
     */
    @Override
    public void processCleanCache() {
        //1.清理分布式缓存Redis中 key以"seckill:"全部删除
        Set keys = redisTemplate.keys("seckill:*");
        redisTemplate.delete(keys);

        //2.清理本地JVM缓存  TODO 发送Redis订阅消息，通知所有秒杀节点全部清理本地缓存
        seckillGoodsCache.invalidateAll();
    }
}
